/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import socket from '@ohos.net.socket';
import Url from '@ohos.url'
import Request from './Request';
import buffer from '@ohos.buffer';
import { Logger } from './utils/Logger';
import ConstantManager from './ConstantManager'
import connection from '@ohos.net.connection';

export class Http {
    public static createHttp(): HttpRequest {
        let httpRequest = new HttpRequest()
        return httpRequest
    }
}

export class HttpRequest {
    private ALPNProtocols: Array<string> = ["spdy/1", "http/1.1"]
    tlsSocket: socket.TLSSocket
    tcp: socket.TCPSocket;
    callback: AsyncCallback<HttpResponse>
    isClose: boolean = true
    secureOptions: socket.TLSSecureOptions = {
        ca: [],
        cert: '',
        key: '',
        passwd: '',
        protocols: [socket.Protocol.TLSv12],
        useRemoteCipherPrefer: true,
        signatureAlgorithms: '',
        cipherSuite: ''
    }

    constructor() {
        this.isClose = false
    }

    private dealSuccessResult(res) {
        if (this.callback) {
            let response: HttpResponse = new HttpResponse()

            let arr: string[] = res.split("\r\n");

            for (let i = 0; i < arr.length; i++) {
                if (arr[i].endsWith("OK")) {
                    let status = arr[i].split(' ')
                    response.responseCode = Number.parseInt(status[1])
                    break
                }
            }
            this.log('response length >> ' + arr.length)

            var data = ''
            for (let r = 3;r < arr.length; r++) {
                if (Number.parseInt(arr[r]) >= 0) {
                    data += arr[r+1]
                }
            }
            response.result = data

            let headerMap = new Map()
            for (var i = 1; i <= arr.length - 2; i++) {
                let item = arr[i].split('\:')
                if (item.length > 1) {
                    headerMap.set(item[0], arr[i].substring(item[0].length + 1, arr[i].length).trim())
                }
            }
            response.header = Object.fromEntries(headerMap.entries())

            this.log('请求完成 >> ' + JSON.stringify(response))
            this.callback(null, response)
        }
    }

    private dealFailResult(code: number, msg: string, name: string) {
        if (this.callback) {
            let errContent = {
                code: code,
                message: msg,
                name: name
            }
            this.log('请求失败：' + JSON.stringify(errContent))
            this.callback(errContent, null)
        }
    }

    private bind(callback) {
        if (!!this.tcp) {
            let that = this;
            connection.getDefaultNet().then(function (netHandle) {
                connection.getConnectionProperties(netHandle, function (error, info) {
                    that.log("NET" + JSON.stringify(info))
                    let ip = info.linkAddresses[0].address.address
                    let family = info.linkAddresses[0].address.family;
                    that.log("ip = " + ip);
                    that.tcp.bind({ address: ip }, err => {
                        if (err) {
                            that.log('bind tcp fail  ' + JSON.stringify(err))
                            that.dealFailResult(err.code, 'bind fail', err.name)
                            return;
                        }
                        that.log('bind tcp success');
                        callback()
                    })
                    return
                })
            })

        }

        if (!!this.tlsSocket) {
            let that = this;
            connection.getDefaultNet().then(function (netHandle) {
                connection.getConnectionProperties(netHandle, function (error, info) {
                    let ip = info.linkAddresses[0].address.address
                    let family = info.linkAddresses[0].address.family;
                    that.log("ip = " + ip);
                    that.tlsSocket.bind({ address: ip }, err => {
                        if (err) {
                            that.log('bind tls fail  ' + JSON.stringify(err))
                            that.dealFailResult(err.code, 'bind fail', err.name)
                            return;
                        }
                        that.log('bind tls success');
                        callback()
                    })
                })
            })

        }
    }

    private connect(url: string, options: HttpRequestOptions, callback) {
        let urlObject = Url.URL.parseURL(url);
        let host = urlObject.hostname
        let port
        let pathname = urlObject.pathname
        let search = urlObject.search

        if (url.startsWith("http://")) {
            port = urlObject.port || '8080'
        }

        if (url.startsWith("https://")) {
            port = urlObject.port || '443'
        }

        this.log(`connecting......    url:${url} , host:${host} , port:${port} , pathname=${pathname} , search=${search}`)

        if (!!this.tcp) {
            this.tcp.connect({
                address: { address: host, port: Number.parseInt(port), family: 1 },
                timeout: options.connectTimeout
            }).then(() => {
                this.log('connect tcp success')
                callback()
            }).catch((err) => {
                this.log('connect tcp fail  ' + JSON.stringify(err))
                this.dealFailResult(err.code, 'connect fail', err.name)
            })
            return
        }

        if (!!this.tlsSocket) {
            let options = {
                ALPNProtocols: this.ALPNProtocols,
                address: {
                    address: host,
                    port: Number.parseInt(port),
                    family: 1
                },
                secureOptions: this.secureOptions,
            }
            this.tlsSocket.connect(options, (err, data) => {
                if (!err) {
                    this.log('connect tls success')
                    callback()
                    return
                }
                this.log('connect tls fail  ' + JSON.stringify(err))
                this.dealFailResult(err.code, 'connect fail', err.name)
            });
        }
    }

    private send(url: string, request: Request, options: HttpRequestOptions) {
        let sendBody = ''

        let urlObject = Url.URL.parseURL(url);
        let host = urlObject.hostname || ''
        let port
        let pathname = urlObject.pathname || ''
        let search = urlObject.search || ''

        if (url.startsWith("http://")) {
            port = urlObject.port || '8080'
        }

        if (url.startsWith("https://")) {
            port = urlObject.port || '443'
        }

        this.log(`sending......    url:${url}     host:${host}, port=${port}, pathname=${pathname}, search=${search}`);

        let headers = ''
        if (options.header !== undefined) {
            for (let headerKey in options.header) {
                if (headerKey !== ConstantManager.CONTENT_TYPE) {
                    headers += headerKey + ': ' + options.header[headerKey] + '\r\n'
                }
            }
        }
        this.log('myHeaders = ' + headers.replace(/\r\n/g, "........."))

        switch (options.method.toString()) {
            case 'UPLOAD':
                this.log('===>>>  fileName: ' + JSON.stringify(request.fileName))
                this.log('===>>>  filePath: ' + JSON.stringify(request.filePath))
                this.log('===>>>  files: ' + JSON.stringify(request.files))
                break
            case RequestMethod.GET:
                sendBody += "GET " + pathname + search + " HTTP/1.1\r\n"
                sendBody += "Host: " + host + ":" + port + "\r\n"
                sendBody += "Content-Type: application/json;charset=UTF-8\r\n"
                sendBody += 'Connection: keep-alive\r\n'
                sendBody += "\r\n"
                break
            case RequestMethod.DELETE:
                sendBody += "DELETE " + pathname + search + " HTTP/1.1\r\n"
                sendBody += "Host: " + host + ":" + port + "\r\n"
                sendBody += "Content-Type: application/json;charset=UTF-8\r\n"
                sendBody += 'Connection: keep-alive\r\n'
                sendBody += "\r\n"
                break
            case RequestMethod.PUT:
                let pa = ''
                if (typeof options.extraData == 'string' && options.extraData.length > 0) {
                    if (options.extraData.startsWith('{')) {
                        let obj = JSON.parse(options.extraData)
                        pa += '?'
                        for (let objKey in obj) {
                            pa += objKey + '=' + obj[objKey] + '&'
                        }
                        pa = pa.substring(0, pa.length - 1)
                    } else {
                        pa = options.extraData
                    }
                }

                sendBody += "PUT " + pathname + pa + " HTTP/1.1\r\n"
                sendBody += "Host: " + host + ":" + port + "\r\n"
                sendBody += 'Content-Type: application/json; charset=utf8\r\n'
                sendBody += 'Connection: keep-alive\r\n'
                if (headers.length > 0) {
                    sendBody += headers
                }
                sendBody += '\r\n'
                break
            case RequestMethod.POST:
                let params = ''
                if (search.length > 1) {
                    params = search.substring(1, search.length)
                } else if (typeof options.extraData == 'string' && options.extraData.length > 0) {
                    if (options.extraData.startsWith('{')) {
                        let obj = JSON.parse(options.extraData)
                        for (let objKey in obj) {
                            params += objKey + '=' + obj[objKey] + '&'
                        }
                        params = params.substring(0, params.length - 1)
                    } else {
                        params = options.extraData
                    }
                }

                this.log('params = ' + params.replace(/\r\n/g, "........."))

                sendBody += "POST " + pathname + " HTTP/1.1\r\n"
                sendBody += "Host: " + host + ":" + port + "\r\n"
                sendBody += 'Content-Type: application/x-www-form-urlencoded\r\n'
                if (headers.length > 0) {
                    sendBody += headers
                }
                if (params.length > 0) {
                    sendBody += 'Content-Length: ' + params.length + '\r\n'
                }
                sendBody += '\r\n'
                if (params.length > 0) {
                    sendBody += params + '\r\n'
                }
                sendBody += '\r\n'
                break
        }
        this.sendBody(sendBody)
    }

    private sendBody(body: string) {
        this.log("send body = " + body.toString().replace(/\r\n/g, " ........."));

        let bodys = body.split("\r\n")
        for (var i = 0; i < bodys.length; i++) {
            this.log("request body -----" + bodys[i]);
        }

        if (!!this.tlsSocket) {
            this.tlsSocket.send(body.toString(), (err) => {
                if (!err) {
                    this.log('tls send success')
                } else {
                    this.log('tls send fail  ' + JSON.stringify(err))
                    this.dealFailResult(err.code, 'send fail', err.name)
                }
            });
            return
        }

        if (!!this.tcp) {
            this.tcp.send({ data: body.toString(), encoding: 'UTF-8' })
                .then(() => {
                    this.log('tcp send success')
                })
                .catch((err) => {
                    this.log('tcp send fail  ' + JSON.stringify(err))
                    this.dealFailResult(err.code, 'send fail', err.name)
                })
        }
    }

    /**
     * Initiates an HTTP request to a given URL.
     *
     * @param url URL for initiating an HTTP request.
     * @param options Optional parameters {@link HttpRequestOptions}.
     * @param callback Returns {@link HttpResponse}.
     * @permission ohos.permission.INTERNET
     */
    public request(url: string, request: Request, options: HttpRequestOptions, callback: AsyncCallback<HttpResponse>) {
        this.log('url >>>>>>>>>>>>>>>>>>>  ' + url)
        if (url.startsWith("http://")) {
            this.tcp = socket.constructTCPSocketInstance();
        }

        if (url.startsWith("https://")) {
            this.tlsSocket = socket.constructTLSSocketInstance();
        }

        this.log('option >>>>>>>>>>>>>>>>  ' + JSON.stringify(options))
        this.log('request >>>>>>>>>>>>>>>  ' + JSON.stringify(request))
        this.callback = callback
        this.bind(() => {
            this.on("message", value => {
                let response = buffer.from(value.message).toString("utf-8")
                this.log("message:" + JSON.stringify(value))
                this.dealSuccessResult(response)
            });

            this.on('error', err => {
                if (!this.isClose) {
                    this.log("on: error  err:" + JSON.stringify(err))
                    this.dealFailResult(err.code, err.message, err.name)
                }
            });

            this.on('close', () => {
                this.isClose = true
                this.log("on: close")
            });

            this.connect(url, options, () => {
                this.send(url, request, options)
            })
        })
    }

    /**
     * 设置CA证书
     * @param caStr
     */
    public setCaData(caStr: string[]) {
        this.secureOptions.ca = caStr;
    }

    /**
     * Destroys an HTTP request.
     */
    public destroy() {
        this.log('the tcp is reading close')
        if (!!this.tcp) {
            this.tcp.close()
                .then(() => {
                    this.log('the tcp is closed')
                })
                .catch(err => {
                    this.log('the tcp close fail!')
                })
        }
        //        if (!!this.tlsSocket) {
        //            this.tlsSocket.close().then(()=>{
        //                this.log('the tlsSocket is closed')
        //            })
        //        }

    }

    public on(type: string, callback: Callback<any>) {
        if (!!this.tcp) {
            switch (type) {
                case 'message':
                    this.tcp.on('message', (v) => {
                        callback(v)
                    });
                    break
                case 'connect':
                    this.tcp.on('connect', (v) => {
                        callback(null)
                    });
                    break
                case 'close':
                    this.tcp.on('close', (v) => {
                        callback(null)
                    });
                    break
                case 'error':
                    this.tcp.on('error', (v) => {
                        callback(v)
                    });
                    break
            }
            return
        }

        if (!!this.tlsSocket) {
            switch (type) {
                case 'message':
                    this.tlsSocket.on('message', (v) => {
                        callback(v)
                    });
                    break
                case 'connect':
                    this.tlsSocket.on('connect', (v) => {
                        callback(null)
                    });
                    break
                case 'close':
                    this.tlsSocket.on('close', (v) => {
                        callback(null)
                    });
                    break
                case 'error':
                    this.tlsSocket.on('error', (err) => {
                        callback(err)
                    });
                    break
            }
            return
        }
    }

    public off(type: string, callback: Callback<Object>) {
        let socket = !!this.tcp ? this.tcp : this.tlsSocket
        switch (type) {
            case 'message':
                socket.off('message', (v) => {
                    callback(v)
                });
                break
            case 'connect':
                socket.off('connect', (v) => {
                    callback(null)
                });
                break
            case 'close':
                socket.off('close', (v) => {
                    callback(null)
                });
                break
            case 'error':
                socket.off('error', (v) => {
                    callback(v)
                });
                break
        }
    }

    private log(content) {
        Logger.info('HTTP REQUEST -->>> ' + content)
    }
}

export interface HttpRequestOptions {
    /**
     * Request method.
     */
    method?: RequestMethod; // default is GET
    /**
     * Additional data of the request.
     * extraData can be a string or an Object (API 6) or an ArrayBuffer(API 8).
     */
    extraData?: string | Object | ArrayBuffer;
    /**
     * HTTP request header.
     */
    header?: Object; // default is 'content-type': 'application/json'
    /**
     * Read timeout period. The default value is 60,000, in ms.
     */
    readTimeout?: number; // default is 60s
    /**
     * Connection timeout interval. The default value is 60,000, in ms.
     */
    connectTimeout?: number; // default is 60s.
}

export enum RequestMethod {
    OPTIONS = "OPTIONS",
    GET = "GET",
    HEAD = "HEAD",
    POST = "POST",
    PUT = "PUT",
    DELETE = "DELETE",
    TRACE = "TRACE",
    CONNECT = "CONNECT"
}

export enum ResponseCode {
    OK = 200,
    CREATED,
    ACCEPTED,
    NOT_AUTHORITATIVE,
    NO_CONTENT,
    RESET,
    PARTIAL,
    MULT_CHOICE = 300,
    MOVED_PERM,
    MOVED_TEMP,
    SEE_OTHER,
    NOT_MODIFIED,
    USE_PROXY,
    BAD_REQUEST = 400,
    UNAUTHORIZED,
    PAYMENT_REQUIRED,
    FORBIDDEN,
    NOT_FOUND,
    BAD_METHOD,
    NOT_ACCEPTABLE,
    PROXY_AUTH,
    CLIENT_TIMEOUT,
    CONFLICT,
    GONE,
    LENGTH_REQUIRED,
    PRECON_FAILED,
    ENTITY_TOO_LARGE,
    REQ_TOO_LONG,
    UNSUPPORTED_TYPE,
    INTERNAL_ERROR = 500,
    NOT_IMPLEMENTED,
    BAD_GATEWAY,
    UNAVAILABLE,
    GATEWAY_TIMEOUT,
    VERSION
}

export class HttpResponse {
    /**
     * result can be a string (API 6) or an ArrayBuffer(API 8). Object is deprecated from API 8.
     */
    result: string | Object | ArrayBuffer;
    /**
     * Server status code.
     */
    responseCode: ResponseCode | number;
    /**
     * All headers in the response from the server.
     */
    header: Object;
    /**
     * @since 8
     */
    cookies: string;
}

export interface AsyncCallback<T, E = void> {
    /**
     * Defines the callback data.
     * @since 6
     */
    (err: BusinessError<E>, data: T): void;
}

export interface BusinessError<T = void> extends Error {
    /**
     * Defines the basic error code.
     * @since 6
     */
    code: number;
    /**
     * Defines the additional information for business
     * @type { ?T }
     * @since 9
     */
    data?: T;
}

export interface Callback<T> {
    /**
     * Defines the callback info.
     * @since 6
     */
    (data: T): void;
}